"""
DoublyCircularLinkedList is a data structure that combines the properties of both a doubly linked list and a circular linked list. It has nodes that contain a reference to the next node and the previous node, forming a circular chain.

Properties:
- Each node in a DoublyCircularLinkedList contains three parts: data, a reference to the next node, and a reference to the previous node.
- The first node in the list is called the head, and the last node is called the tail. The head's previous reference points to the tail, and the tail's next reference points to the head, creating a circular structure.
- DoublyCircularLinkedList allows for bidirectional traversal, meaning that we can traverse both forward and backward.
- The list can be traversed starting from any node, as the circular structure ensures that all nodes are reachable.

Operations:

1. Initialization:
   - `DoublyCircularLinkedList()`: Creates an empty doubly circular linked list.

2. Basic Operations:
   - `add_front(data)`: Adds an element to the front of the doubly circular linked list.
   - `add_end(data)`: Adds an element to the end of the doubly circular linked list.
   - `insert(data, position)`: Inserts an element at the specified position in the doubly circular linked list.
   - `remove(data)`: Removes the first occurrence of the specified element from the doubly circular linked list.
   - `remove_at(position)`: Removes the element at the specified position from the doubly circular linked list.
   - `search(data)`: Searches for the first occurrence of the specified element in the doubly circular linked list and returns its position.
   - `get(position)`: Returns the element at the specified position in the doubly circular linked list.
   - `size()`: Returns the number of elements in the doubly circular linked list.
   - `is_empty()`: Checks if the doubly circular linked list is empty.

Example Usage:
```
doubly_circular_linked_list = DoublyCircularLinkedList()

doubly_circular_linked_list.add_front(3)
doubly_circular_linked_list.add_end(7)
doubly_circular_linked_list.insert(5, 1)
doubly_circular_linked_list.remove(3)
print(doubly_circular_linked_list.search(7))  # Output: 0
print(doubly_circular_linked_list.get(0))  # Output: 7
print(doubly_circular_linked_list.size())  # Output: 2
print(doubly_circular_linked_list.is_empty())  # Output: False
```

Time and Space Complexity:
- Adding or removing elements at the front or end of a doubly circular linked list has a time complexity of O(1).
- Inserting or removing elements at a specific position in the doubly circular linked list has a time complexity of O(n), where n is the number of elements.
- Searching for an element in the doubly circular linked list has a time complexity of O(n), as it may require traversing the entire list.
- The space complexity of a doubly circular linked list is O(n), where n is the number of elements, as each element requires a node with references to the previous and next nodes.

"""

class Node:
    def __init__(self, value=None):
        """
        Initialize a node for use in a doubly circular linked list.

        Args:
            value: The value to be stored in the node.
            next: The reference to the next node in the list. Defaults to None.
            prev: The reference to the previous node in the list. Defaults to None.
        """
        self.value = value
        self.next = None
        self.prev = None


class DoublyCircularLinkedList:
    def __init__(self):
        """
        Initialize an empty doubly circular linked list.

        Attributes:
            head (Node): The first node in the list.
            length (int): The number of elements in the list.
        """
        self.head = None
        self.length = 0

    def append(self, value):
        """
        Insert a new element at the end of the list.

        Args:
            value: The value to be stored in the new node.
        """
        new_node = Node(value=value)
        if self.head is None:
            self.head = new_node
            new_node.next = new_node
            new_node.prev = new_node
        else:
            last_node = self.head.prev
            last_node.next = new_node
            new_node.prev = last_node
            new_node.next = self.head
            self.head.prev = new_node
        self.length += 1

    def prepend(self, value):
        """
        Insert a new element at the beginning of the list.

        Args:
            value: The value to be stored in the new node.
        """
        new_node = Node(value=value)
        if self.head is None:
            self.head = new_node
            new_node.next = new_node
            new_node.prev = new_node
        else:
            last_node = self.head.prev
            new_node.next = self.head
            new_node.prev = last_node
            self.head.prev = new_node
            last_node.next = new_node
            self.head = new_node
        self.length += 1

    def find_by_index(self, index):
        """
        Find and return a node at a particular index.

        Args:
            index (int): The index of the node.

        Returns:
            The node at the specified index. If the index is out of range, return None.
        """
        if index < 0 or index >= self.length:
            return None
        current = self.head
        for _ in range(index):
            current = current.next
        return current

    def find_by_value(self, value):
        """
        Find and return a node with a particular value.

        Args:
            value: The value to search for.

        Returns:
            The first node that contains the value. If no such node exists, return None.
        """
        current = self.head
        for _ in range(self.length):
            if current.value == value:
                return current
            current = current.next
            if current == self.head:
                break
        return None

    def find_min_node(self):
        """
        Find and return the node with the minimum value.

        Returns:
            The node with the smallest value. If the list is empty, return None.
        """
        if self.head is None:
            return None
        min_node = self.head
        current = self.head.next
        while current != self.head:
            if current.value < min_node.value:
                min_node = current
            current = current.next
        return min_node

    def delete_by_index(self, index):
        """
        Delete the node at the specified index.

        Args:
            index (int): The index of the node to delete.

        Raises:
            IndexError: If the index is out of range.
        """
        if index < 0 or index >= self.length:
            raise IndexError('DoublyCircularLinkedList index out of range')
        if self.length == 1:
            self.head = None
        else:
            node_to_delete = self.find_by_index(index)
            node_to_delete.prev.next = node_to_delete.next
            node_to_delete.next.prev = node_to_delete.prev
            if node_to_delete == self.head:
                self.head = node_to_delete.next
        self.length -= 1

    def delete_by_value(self, value):
        """
        Delete the node with a particular value.

        Args:
            value: The value to search for and delete.

        Raises:
            ValueError: If the value is not found in the linked list.
        """
        if self.head is None:
            raise ValueError('Value not found in linked list')
        if self.head.value == value:
            if self.length == 1:
                self.head = None
            else:
                self.head.prev.next = self.head.next
                self.head.next.prev = self.head.prev
                self.head = self.head.next
        else:
            current = self.head.next
            while current != self.head:
                if current.value == value:
                    current.prev.next = current.next
                    current.next.prev = current.prev
                    self.length -= 1
                    return
                current = current.next
            raise ValueError('Value not found in linked list')

    def __len__(self):
        """
        Return the number of elements in the doubly circular linked list.

        Returns:
            int: The number of elements in the doubly circular linked list.
        """
        return self.length

    def __repr__(self):
        """
        Return a machine-readable string representation of the DoublyCircularLinkedList.

        Returns:
            str: A string representation of the DoublyCircularLinkedList in the format '<DoublyCircularLinkedList: head_value, length>'.
        """
        if self.head:
            head_value = self.head.value
        else:
            head_value = None

        return f'<DoublyCircularLinkedList: head={head_value}, length={self.length}>'

    def __str__(self):
        """
        Convert the doubly circular linked list to a string representation.

        Returns:
            str: A string representation of the doubly circular linked list in the format 'DoublyCircularLinkedList: [value1 <-> value2 <-> ... <-> valueN <-> head_value]'.
        """
        values = []
        current = self.head
        if current is not None:
            while True:
                values.append(str(current.value))
                current = current.next
                if current == self.head:
                    break
        return 'DoublyCircularLinkedList: [' + ' <-> '.join(values) + f' <-> {self.head.value}]'


if __name__ == "__main__":
    linked_list = DoublyCircularLinkedList()

    linked_list.append(5)
    linked_list.append(10)
    linked_list.append(15)
    linked_list.prepend(1)

    print(linked_list)  # DoublyCircularLinkedList: [1 <-> 5 <-> 10 <-> 15 <-> 1]

    node = linked_list.find_by_index(2)
    if node:
        print(f"Value at index 2: {node.value}")  # Value at index 2: 10
    else:
        print("Index out of range.")

    node = linked_list.find_by_value(10)
    if node:
        print("Found node with value 10")  # Found node with value 10
    else:
        print("Value not found in linked list.")

    min_node = linked_list.find_min_node()
    if min_node:
        print(f"Node with minimum value: {min_node.value}")  # Node with minimum value: 1
    else:
        print("DoublyCircularLinkedList is empty.")

    linked_list.delete_by_index(1)
    print(f"DoublyCircularLinkedList after deleting value at index 1: {linked_list}")  # DoublyCircularLinkedList after deleting value at index 1: [1 <-> 10 <-> 15 <-> 1]

    linked_list.delete_by_value(15)
    print(f"DoublyCircularLinkedList after deleting value 15: {linked_list}")  # DoublyCircularLinkedList after deleting value 15: [1 <-> 10 <-> 1]

    print(repr(linked_list))  # <DoublyCircularLinkedList: head=1, length=2>
